#define NUL_TERMINATOR_LENGTH 4

class GIconv {
    public:
    GIconv(const char* toEncType, const char* fromEncType);
    ~GIconv();

    void* convertBytes(const void* bytes, size_t len, size_t* bytesRead, size_t* bytesWrite);

    void setFallback(const string& fallback)
    {
        _fallback = fallback;
    }

    private:
    void open(const char* toEncType, const char* fromEncType);
    void close();
    void* doConvert(const void* bytes, size_t len, size_t* bytesRead, size_t* bytesWrite, const std::string& fallback = "");
    private:
    iconv_t _cvt;
    std::string _fallback;
    std::string _fromEnc;
    std::string _toEnc;
};

GIconv::GIconv(const char* toEncType, const char* fromEncType)
: _toEnc(toEncType)
, _fromEnc(fromEncType)
{
    open(toEncType, fromEncType);
}

GIconv::~GIconv()
{
    close();
}

void GIconv::open(const char* toEncType, const char* fromEncType)
{
    _cvt = ::iconv_open(toEncType, fromEncType);

    if (_cvt == (iconv_t) -1) {
        throw FormatException(cflResLoader, _T("IDS_CONV_FAIL_CONVERT"));
    }
}

void GIconv::close()
{
    if (_cvt != (iconv_t) -1) {
        ::iconv_close(_cvt);
        _cvt = (iconv_t) -1;
    }
}

#ifdef __LINUX__
#define CHARPP (char **)
#else
#define CHARPP (const char**)
#endif

void*
GIconv::doConvert(const void* bytes, size_t len, size_t* bytesRead, size_t* bytesWrite, const std::string& fallback)
{
    const char *str = (const char*) bytes;
    const char *p = (const char*) bytes;
    if (len < 0) {
        len = strlen(p);
    }

    size_t inbytes_remaining = len;
    size_t outbuf_size = len + NUL_TERMINATOR_LENGTH;
    size_t outbytes_remaining = outbuf_size - NUL_TERMINATOR_LENGTH;

    char* dest = (char*) malloc(outbuf_size * 1.5);
    char* outp = dest;
    bool have_error = false;
    bool done = false;
    bool reset = false;
    size_t err;

    while (!done && !have_error) {
        if (reset) { // reset the state
            err = ::iconv(_cvt, NULL, &inbytes_remaining, &outp, &outbytes_remaining);
        }
        else { // do the convert
            err = ::iconv(_cvt, CHARPP&p, &inbytes_remaining, &outp, &outbytes_remaining);
        }

        if (err == (size_t) -1) {
            switch (errno) {
                case EINVAL:
                    /* Incomplete text, do not report an error */
                    done = true;
                    break;

                case E2BIG: {
                                size_t used = outp - dest;

                                outbuf_size *= 2;
                                dest = (char*) realloc(dest, outbuf_size);

                                outp = dest + used;
                                outbytes_remaining = outbuf_size - used - NUL_TERMINATOR_LENGTH;
                }
                    break;

                case EILSEQ: {
                                 if (fallback.empty() == false) {
                                     p++;
                                     inbytes_remaining--;
                                     memcpy(outp, _fallback.c_str(), _fallback.length());
                                     outbytes_remaining -= _fallback.length();
                                 }
                                 else {
                                     have_error = true;
                                 }
                }
                    break;

                default:
                    have_error = true;
                    break;
            }
        }
        else {
            if (!reset) {
                /* call iconv with NULL inbuf to cleanup shift state */
                reset = true;
                inbytes_remaining = 0;
            }
            else {
                done = true;
            }
        }
    }

    memset(outp, 0, NUL_TERMINATOR_LENGTH);

    if (bytesRead) {
        *bytesRead = p - str;
    }
    else {
        if ((p - str) != len) {
            if (!have_error) {
                printf("Partial character sequence at end of input\n");
                have_error = true;
            }
        }
    }

    if (bytesWrite) {
        *bytesWrite = outp - dest;    /* Doesn't include '\0' */
    }

    if (have_error) {
        free(dest);
        dest = NULL;
    }

    return dest;
}

void*
GIconv::convertBytes(const void * bytes, size_t len, size_t* bytesRead, size_t* bytesWrite)
{
    void* result = doConvert(bytes, len, bytesRead, bytesWrite);
    if (result == NULL) {
        close();
        open(_toEnc.c_str(), "GBK");
        result = doConvert(bytes, len, bytesRead, bytesWrite);
    }

    if (result == NULL) {
        close();
        open(_toEnc.c_str(), _fromEnc.c_str());
        result = doConvert(bytes, len, bytesRead, bytesWrite, _fallback);
    }
    return result;
}

